PlantUML live editor on Scrapbox
即席コード
code:pu
Bob->Alice: Hello
Alice->Bob: 誰ダお前
Bob->Alice: お前こそ誰だよ
Alice->Bob: そっちが先に答えろ
code:tsx
const { mount } = await import("./App.tsx");
mount();
code:App.tsx
/** @jsx h */
/** @jsxFrag Fragment */
import {
h,
Fragment,
render,
} from "../preact@10.5.14/mod.js";
import {
useState,
useCallback,
useEffect,
} from "../preact@10.5.14/hooks.js";
import { useCodeBlock } from "./useCodeBlock.ts";
import { toPlantUML } from "./toPlantUML.ts";
interface AppProps {
close: () => void;
}
function App({ close }: AppProps) {
const files = useCodeBlock("pu");
const url ,setUrl = useState<URL | undefined>(undefined); // URLの更新
useEffect(() => setUrl(
files0 ? toPlantUML(files0.lines.join("\n")) : undefined エラーの場合はpng画像を代わりに出す
png画像だとエラー内容も表示してくれるようだ
2022-01-26 07:20:17 Hooksを経由せずにsrcを書き換えるようにした
code:App.tsx
const onError = useCallback(
(e: h.JSX.GenericEventHandler<HTMLImageElement>) =>
e.currentTarget.src = e.currentTarget.src.replace("svg", "png"),
[]
);
const onClose = useCallback(() => close(), close); return (
<>
<style>{`
.container {
background-color: var(--page-bg);
border: 1px solid hsl(72, 64%, 57%);
border-radius: 3px;
}
.pin {
position: fixed;
top: 10px;
left: 10%;
width: 80%;
max-height: 50%;
overflow-y: auto;
z-index: 9999;
}
position: absolute;
top: 0;
right: 0;
}
`}</style>
<div id="preview" className="container pin">
<button id="close" onClick={onClose}>
x
</button>
{url && (<img src={url} crossorigin="anonymous" onError={onError} />)}
</div>
</>
);
}
export function mount() {
const app = document.createElement("div");
const shadowRoot = app.attachShadow({ mode: "open" });
document.body.append(app);
render(<App close={() => app.remove()} />, shadowRoot);
}
特定のcodeBlockを取得する
code:useCodeBlock.ts
import {
useState,
useEffect,
} from "../preact@10.5.14/hooks.js";
import { throttle } from "./throttle.ts";
export function useCodeBlock(lang: string) {
useEffect(() => {
const callback = throttle(() => {
const files = getCodeFiles();
setFiles(files.filter((file) => file.lang === lang));
}, {trailing: true, interval: 300});
scrapbox.addListener("lines:changed", callback);
callback();
return () =>
scrapbox.removeListener("lines:changed", callback);
},[]);
return files;
}
export interface File {
filename?: string;
lang: string;
/** コードブロックの開始行のid */ startIds: string[];
lines: string[];
}
function getCodeFiles() {
const codeBlocks =
scrapbox.Page.lines?.flatMap((line) => "codeBlock" in line ? line : []) ?? [];
return codeBlocks.reduce((acc: File[], { codeBlock, text, id }) => {
const sameFileIndex = acc.findIndex(({ filename }) =>
filename !== undefined && filename === codeBlock.filename
);
// code blockの先頭かつ新しいコードブロックのときのみ新しいfileを追加する
if (codeBlock.start && sameFileIndex < 0) {
return [...acc, {
filename: codeBlock.filename,
lang: codeBlock.lang,
lines: [] as string[],
}];
}
if (codeBlock.start) {
acc.at(sameFileIndex)?.startIds?.push?.(id);
} else {
// 既存のコードブロックもしくは末尾のコードブロックに追記する
acc.at(sameFileIndex)?.lines?.push?.(text);
}
return acc;
}, []);
}
code:toPlantUML.ts
import { deflate } from "../denoflate-min/mod.js";
import { encode64, textToBuffer } from "./encode.ts";
export function toPlantUML(uml: string, type: "svg" | "png" = "svg") {
encode64(deflate(textToBuffer(uml), 9))
}`;
}
code:encode.ts
export function encode64(data: Uint8Array) {
let r = "";
for (let i = 0; i < data.length; i += 3) {
if (i + 2 === data.length) {
r += append3bytes(datai, datai + 1, 0); } else if (i + 1 === data.length) {
r += append3bytes(datai, 0, 0); } else {
}
}
return r;
}
function encode6bit(b: number) {
if (b < 10) return String.fromCharCode(48 + b);
b -= 10;
if (b < 26) return String.fromCharCode(65 + b);
b -= 26;
if (b < 26) return String.fromCharCode(97 + b);
b -= 26;
if (b === 0) return '-';
if (b === 1) return '_';
return '?';
}
function append3bytes(b1: number, b2: number, b3: number) {
const c1 = b1 >> 2;
const c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
const c3 = ((b2 & 0xF) << 2) | (b3 >> 6);
const c4 = b3 & 0x3F;
return encode6bit(c1 & 0x3F)
+ encode6bit(c2 & 0x3F)
+ encode6bit(c3 & 0x3F)
+ encode6bit(c4 & 0x3F);
}
export function textToBuffer(text: string) {
const ascii_string = unescape(encodeURIComponent(text)); // 間にこれを噛まさないと文字化けする
let buffer = new Uint8Array(ascii_string.length);
for (let i = 0; i < ascii_string.length; i++) {
bufferi = ascii_string.charCodeAt(i); }
return buffer;
}
Throttle
code:throttle.ts
function p(n){return new Promise(s=>setTimeout(s,n))}function v(n,s){let{trailing:f=!1,interval:i=0}=s??{},t,r=!1,l=e=>{t?.resolve?.({executed:!1}),t=e},m=()=>{let{...e}=t;return t=void 0,e},c=async()=>{if(r||!t)return;r=!0,i>0&&await p(i);let{parameters:e,resolve:o,reject:u}=m();try{let a=await n(...e);r=!1,o({result:a,executed:!0})}catch(a){r=!1,u(a)}finally{f?await c():(l(),await Promise.resolve())}};return(...e)=>new Promise((o,u)=>{l({parameters:e,resolve:o,reject:u}),c()})}export{v as throttle};
#2022-01-31 15:45:02 useCodeBlockで取得する言語を外部から指定するように変えた